Pourquoi pas débruiter un chat ? =^.^=¶


Auteurs¶

  • Maxence KAMIONKA — @github
  • Mikhaïl BENALI — @github

L'objectif, via ce projet est de pouvoir générer, en sortie, des images de chat en retirant le bruit présent.

On va donc tout d'abord construire un Autoencoder pour produire un Denoising Autoencoder.

Puisque les résultats de celui-ci restent limités. On va alors construire un réseau U-Net pour obtenir générer des images plus proches de celles en entrée.

Enfin on utilisera Stable Diffusion XL (SDXL) pour essayer de produire une image un peu plus détaillé (pour atténuer le flou obtenu lors de la génération).

Dataset¶

Le jeu de données provient de Kaggle.

Il contient plus de 9 000 images de chats et, pour chaque image, un fichier .cat avec la position des caractéristiques importantes du chat (yeux, nez, oreilles, etc.).

Les données d'annotation sont stockées dans un fichier portant le nom de l'image correspondante plus « cat » à la fin. Il y a un fichier d'annotation pour chaque image de chat. Pour chaque fichier d'annotation, les données d'annotation sont stockées dans l'ordre suivant :

  • Nombre de points (9 par défaut)
  • Œil gauche
  • Œil droit
  • Bouche
  • Oreille gauche-1
  • Oreille gauche-2
  • Oreille gauche-3
  • Oreille droite-1
  • Oreille droite-2
  • Oreille droite-3

Import the libraries¶

In [1]:
import kagglehub
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from PIL import Image
import shutil
import os

import torch
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset

from sklearn.metrics import accuracy_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
C:\Users\maxka\anaconda3\envs\dl_clean\lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
In [2]:
print("CUDA dispo ?", torch.cuda.is_available())
print("Nom du GPU :", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "Pas de GPU")
CUDA dispo ? True
Nom du GPU : NVIDIA GeForce RTX 4080 SUPER

Download the dataset¶

In [3]:
path = kagglehub.dataset_download("crawford/cat-dataset")

print("Path to dataset files:", path)
Warning: Looks like you're using an outdated `kagglehub` version, please consider updating (latest version: 0.3.12)
Path to dataset files: C:\Users\maxka\.cache\kagglehub\datasets\crawford\cat-dataset\versions\2

Inspect the dataset¶

List the files in the dataset directory¶

In [4]:
print("Files in the dataset directory:")
for folder in os.listdir(path):
    print(f"Folder {folder} has {len(os.listdir(os.path.join(path, folder)))} files ")

if 'data' not in os.listdir():
    os.mkdir("data")
Files in the dataset directory:
Folder cats has 0 files 
Folder CAT_00 has 0 files 
Folder CAT_01 has 0 files 
Folder CAT_02 has 0 files 
Folder CAT_03 has 0 files 
Folder CAT_04 has 0 files 
Folder CAT_05 has 0 files 
Folder CAT_06 has 0 files 

Move the images and the .cat files in different folders¶

In [5]:
if 'images' not in os.listdir("data"):
    os.mkdir("data/images/no_category")
if 'cat_files' not in os.listdir("data"):
    os.mkdir("data/cat_files")

Move the images and the .cat files in different folders¶

In [6]:
for folder in os.listdir(path):
    for file in os.listdir(os.path.join(path, folder)):
        if file.endswith(".cat"):
            shutil.move(os.path.join(path, folder, file), os.path.join("data/cat_files", file))
        else:
            shutil.move(os.path.join(path, folder, file), os.path.join("data/images/no_category", file))
In [7]:
## List the files in the images directory
print(f"The images directory has {len(os.listdir('data/images/no_category'))} files")
## List the files in the cat_files directory
print(f"The cat_files directory has {len(os.listdir('data/cat_files'))} files")
The images directory has 10001 files
The cat_files directory has 9993 files

Data Loading¶

In [8]:
img_height = 150
img_width = 150
In [9]:
cat_folder = "data/images/no_category"

images = []

for filename in os.listdir(cat_folder):
    if filename.endswith(".jpg") or filename.endswith(".png"):
        img_path = os.path.join(cat_folder, filename)
        img = Image.open(img_path)
        img = img.resize((img_height, img_width))
        images.append(np.array(img))
images = np.array(images)
# normalisation des pixels entre 0 et 1
images = images.astype("float32") / 255.0

print(f"images shape: {images.shape}")
images shape: (9993, 150, 150, 3)
In [10]:
plt.figure(figsize=(10, 10))
for i in range(9):
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(images[i])
    plt.axis("off")
plt.show()
No description has been provided for this image

Train/Test split¶

In [11]:
x_train, x_test = train_test_split(images, test_size=0.2, random_state=42)

print(f"x_train shape: {x_train.shape}")
print(f"x_test shape: {x_test.shape}")
x_train shape: (7994, 150, 150, 3)
x_test shape: (1999, 150, 150, 3)

Data augmentation¶

Ici nous transformons les images en 150x150 puis nous les normalisons. Nous augmentons ensuite les images en les retournant horizontalement et en les faisant pivoter afin de créer plus de variations dans les données d'entraînement.

In [12]:
IMG_SIZE = 150

# resize + rescale (normalisation en [0,1])
resize_and_rescale = transforms.Compose([
    transforms.Resize((IMG_SIZE, IMG_SIZE)),
    transforms.ToTensor()  # convertit en float tensor et divise par 255 automatiquement
])
In [13]:
# augmentation des données : flip horizontal+vertical + rotation aléatoire
data_augmentation = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomVerticalFlip(p=0.5),
    transforms.RandomRotation(20),  #  ici on met 20 degrés environ
])

Un autoencodeur convolutionnel :

  • Encodeur (compression) :

3 couches Conv2D + MaxPooling2D réduisent progressivement la taille des images

  • Decodeur (reconstruction) :

3 couches Conv2D + UpSampling2D agrandissent progressivement pour recréer l’image originale

La couche finale Conv2D avec activation sigmoid pour produit une image RGB normalisée

In [14]:
class Autoencoder(nn.Module):
    def __init__(self):
        super(Autoencoder, self).__init__()

        # Encodeur
        self.encoder = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, padding=1),  # padding='same'
            nn.ReLU(),
            nn.MaxPool2d(2, stride=2, padding=1),       

            nn.Conv2d(16, 8, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, stride=2, padding=1),

            nn.Conv2d(8, 8, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, stride=2, padding=1)
        )

        # Décodeur
        self.decoder = nn.Sequential(
            nn.Conv2d(8, 8, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Upsample(scale_factor=2, mode='nearest'),

            nn.Conv2d(8, 8, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Upsample(scale_factor=2, mode='nearest'),

            nn.Conv2d(8, 16, kernel_size=3),  # pas de padding ici, pour correspondre au code du tf
            nn.ReLU(),
            nn.Upsample(scale_factor=2, mode='nearest'),

            nn.Conv2d(16, 3, kernel_size=3, padding=1),
            nn.Sigmoid()  # sortie entre 0 et 1
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)

        # on resize la sortie finale à (150,150)
        x = F.interpolate(x, size=(150,150), mode='bilinear', align_corners=False)

        return x


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
autoencoder = Autoencoder().to(device)


# Optimiseur + loss
optimizer = torch.optim.Adam(autoencoder.parameters()) # ou NAdam, à voir
criterion = nn.MSELoss()

# input tensor doit avoir shape [batch_size, 3, 150, 150]
# (PyTorch utilise channels_first)
dummy_input = torch.randn(1, 3, 150, 150).to(device)
output = autoencoder(dummy_input)
print(output.shape)  # -> torch.Size([1, 3, 150, 150])
torch.Size([1, 3, 150, 150])

Préparer les Dataloaders¶

Les Dataloaders sont des objets qui permettent de charger les données en mini-batchs pour l'entraînement du modèle. Ils sont créés à partir des ensembles d'entraînement et de validation, en appliquant les transformations définies précédemment.

In [15]:
batch_size = 128

x_train = torch.tensor(x_train).permute(0, 3, 1, 2).float()
x_test = torch.tensor(x_test).permute(0, 3, 1, 2).float()


train_dataset = TensorDataset(x_train, x_train)  # autoencodeur : input = target
test_dataset = TensorDataset(x_test, x_test)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
In [16]:
epochs = 150

for epoch in range(epochs):
    autoencoder.train()
    train_loss = 0
    for inputs, targets in train_loader:
        inputs = inputs.to('cuda')
        targets = targets.to('cuda')
        
        optimizer.zero_grad()
        outputs = autoencoder(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item() * inputs.size(0)
    
    train_loss /= len(train_loader.dataset)
    print(f"Epoch {epoch+1}/{epochs}, Loss: {train_loss:.6f}")
Epoch 1/150, Loss: 0.057349
Epoch 2/150, Loss: 0.018815
Epoch 3/150, Loss: 0.016387
Epoch 4/150, Loss: 0.014955
Epoch 5/150, Loss: 0.014164
Epoch 6/150, Loss: 0.013260
Epoch 7/150, Loss: 0.011571
Epoch 8/150, Loss: 0.010900
Epoch 9/150, Loss: 0.010555
Epoch 10/150, Loss: 0.010372
Epoch 11/150, Loss: 0.010184
Epoch 12/150, Loss: 0.010060
Epoch 13/150, Loss: 0.009998
Epoch 14/150, Loss: 0.009840
Epoch 15/150, Loss: 0.009793
Epoch 16/150, Loss: 0.009748
Epoch 17/150, Loss: 0.009594
Epoch 18/150, Loss: 0.009536
Epoch 19/150, Loss: 0.009467
Epoch 20/150, Loss: 0.009649
Epoch 21/150, Loss: 0.009349
Epoch 22/150, Loss: 0.009411
Epoch 23/150, Loss: 0.009321
Epoch 24/150, Loss: 0.009267
Epoch 25/150, Loss: 0.009201
Epoch 26/150, Loss: 0.009174
Epoch 27/150, Loss: 0.009135
Epoch 28/150, Loss: 0.009122
Epoch 29/150, Loss: 0.009072
Epoch 30/150, Loss: 0.009091
Epoch 31/150, Loss: 0.009027
Epoch 32/150, Loss: 0.008982
Epoch 33/150, Loss: 0.008998
Epoch 34/150, Loss: 0.008942
Epoch 35/150, Loss: 0.008945
Epoch 36/150, Loss: 0.008927
Epoch 37/150, Loss: 0.008887
Epoch 38/150, Loss: 0.008853
Epoch 39/150, Loss: 0.008822
Epoch 40/150, Loss: 0.008812
Epoch 41/150, Loss: 0.008814
Epoch 42/150, Loss: 0.008816
Epoch 43/150, Loss: 0.008854
Epoch 44/150, Loss: 0.008716
Epoch 45/150, Loss: 0.008721
Epoch 46/150, Loss: 0.008727
Epoch 47/150, Loss: 0.008743
Epoch 48/150, Loss: 0.008689
Epoch 49/150, Loss: 0.008717
Epoch 50/150, Loss: 0.008696
Epoch 51/150, Loss: 0.008617
Epoch 52/150, Loss: 0.008642
Epoch 53/150, Loss: 0.008604
Epoch 54/150, Loss: 0.008670
Epoch 55/150, Loss: 0.008602
Epoch 56/150, Loss: 0.008568
Epoch 57/150, Loss: 0.008619
Epoch 58/150, Loss: 0.008614
Epoch 59/150, Loss: 0.008534
Epoch 60/150, Loss: 0.008534
Epoch 61/150, Loss: 0.008578
Epoch 62/150, Loss: 0.008543
Epoch 63/150, Loss: 0.008517
Epoch 64/150, Loss: 0.008507
Epoch 65/150, Loss: 0.008485
Epoch 66/150, Loss: 0.008466
Epoch 67/150, Loss: 0.008542
Epoch 68/150, Loss: 0.008449
Epoch 69/150, Loss: 0.008533
Epoch 70/150, Loss: 0.008511
Epoch 71/150, Loss: 0.008400
Epoch 72/150, Loss: 0.008410
Epoch 73/150, Loss: 0.008407
Epoch 74/150, Loss: 0.008426
Epoch 75/150, Loss: 0.008431
Epoch 76/150, Loss: 0.008399
Epoch 77/150, Loss: 0.008376
Epoch 78/150, Loss: 0.008421
Epoch 79/150, Loss: 0.008374
Epoch 80/150, Loss: 0.008345
Epoch 81/150, Loss: 0.008396
Epoch 82/150, Loss: 0.008337
Epoch 83/150, Loss: 0.008356
Epoch 84/150, Loss: 0.008327
Epoch 85/150, Loss: 0.008335
Epoch 86/150, Loss: 0.008310
Epoch 87/150, Loss: 0.008296
Epoch 88/150, Loss: 0.008333
Epoch 89/150, Loss: 0.008319
Epoch 90/150, Loss: 0.008304
Epoch 91/150, Loss: 0.008272
Epoch 92/150, Loss: 0.008308
Epoch 93/150, Loss: 0.008263
Epoch 94/150, Loss: 0.008267
Epoch 95/150, Loss: 0.008225
Epoch 96/150, Loss: 0.008268
Epoch 97/150, Loss: 0.008211
Epoch 98/150, Loss: 0.008211
Epoch 99/150, Loss: 0.008228
Epoch 100/150, Loss: 0.008211
Epoch 101/150, Loss: 0.008226
Epoch 102/150, Loss: 0.008175
Epoch 103/150, Loss: 0.008239
Epoch 104/150, Loss: 0.008214
Epoch 105/150, Loss: 0.008209
Epoch 106/150, Loss: 0.008185
Epoch 107/150, Loss: 0.008130
Epoch 108/150, Loss: 0.008202
Epoch 109/150, Loss: 0.008180
Epoch 110/150, Loss: 0.008150
Epoch 111/150, Loss: 0.008134
Epoch 112/150, Loss: 0.008144
Epoch 113/150, Loss: 0.008127
Epoch 114/150, Loss: 0.008124
Epoch 115/150, Loss: 0.008119
Epoch 116/150, Loss: 0.008182
Epoch 117/150, Loss: 0.008099
Epoch 118/150, Loss: 0.008090
Epoch 119/150, Loss: 0.008123
Epoch 120/150, Loss: 0.008072
Epoch 121/150, Loss: 0.008089
Epoch 122/150, Loss: 0.008120
Epoch 123/150, Loss: 0.008076
Epoch 124/150, Loss: 0.008072
Epoch 125/150, Loss: 0.008100
Epoch 126/150, Loss: 0.008084
Epoch 127/150, Loss: 0.008049
Epoch 128/150, Loss: 0.008033
Epoch 129/150, Loss: 0.008110
Epoch 130/150, Loss: 0.008002
Epoch 131/150, Loss: 0.008065
Epoch 132/150, Loss: 0.008046
Epoch 133/150, Loss: 0.008012
Epoch 134/150, Loss: 0.007999
Epoch 135/150, Loss: 0.008018
Epoch 136/150, Loss: 0.008040
Epoch 137/150, Loss: 0.008014
Epoch 138/150, Loss: 0.008010
Epoch 139/150, Loss: 0.007997
Epoch 140/150, Loss: 0.007981
Epoch 141/150, Loss: 0.008003
Epoch 142/150, Loss: 0.007999
Epoch 143/150, Loss: 0.007997
Epoch 144/150, Loss: 0.007967
Epoch 145/150, Loss: 0.007955
Epoch 146/150, Loss: 0.007993
Epoch 147/150, Loss: 0.007954
Epoch 148/150, Loss: 0.007963
Epoch 149/150, Loss: 0.007992
Epoch 150/150, Loss: 0.007971

Reconstruction visualization¶

Nous visualisons les images originales et reconstruites pour évaluer la performance de l'autoencodeur.

In [17]:
autoencoder.eval()  # mode évaluation

# On prend les 10 premières images du test
original_images = x_test[:10].to('cuda')

with torch.no_grad():
    reconstructed = autoencoder(original_images)

# On remet sur CPU et on convertit en numpy (channels_last)
original_images = original_images.cpu().permute(0, 2, 3, 1).numpy()
reconstructed = reconstructed.cpu().permute(0, 2, 3, 1).numpy()

plt.figure(figsize=(20, 4))
for i in range(10):
    # Image originale
    ax = plt.subplot(2, 10, i + 1)
    plt.imshow(original_images[i])
    plt.axis('off')

    # Image reconstruite
    ax = plt.subplot(2, 10, i + 1 + 10)
    plt.imshow(reconstructed[i])
    plt.axis('off')

plt.show()
No description has been provided for this image

Comme nous pouvons le voir, l'autoencodeur est capable de reconstruire les images de manière reconnaissable mais très floues. Certains détails sont perdus, mais cela montre que l'autoencodeur a appris à capturer les caractéristiques essentielles des images.

Denoising Autoencoder (DAE)¶

Entraînement avec le même autoencoder mais avec une image bruitée en entrée et comme cible l'image propre en sortie.

In [18]:
noise_factor = 0.2

x_train_noisy = x_train + noise_factor * torch.randn_like(x_train)
x_test_noisy = x_test + noise_factor * torch.randn_like(x_test)

# Clip entre 0 et 1
x_train_noisy = torch.clamp(x_train_noisy, 0., 1.)
x_test_noisy = torch.clamp(x_test_noisy, 0., 1.)

Préparer les DataLoaders avec images bruitées en entrée et images propres en sortie (targets)¶

In [19]:
batch_size = 128

train_dataset = TensorDataset(x_train_noisy, x_train)  # input noisy, target clean
test_dataset = TensorDataset(x_test_noisy, x_test)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size)
In [20]:
print(x_train_noisy.shape, x_train.shape)
print(x_train_noisy.dtype, x_train.dtype)

# aucune valeur NaN
print(np.isnan(x_train_noisy).sum())
print(np.isnan(x_train).sum())
torch.Size([7994, 3, 150, 150]) torch.Size([7994, 3, 150, 150])
torch.float32 torch.float32
tensor(0)
tensor(0)

Entraîner l'autoencoder comme DAE¶

In [21]:
epochs = 75

autoencoder.train()
for epoch in range(epochs):
    train_loss = 0
    for noisy_imgs, clean_imgs in train_loader:
        noisy_imgs = noisy_imgs.to('cuda')
        clean_imgs = clean_imgs.to('cuda')
        
        optimizer.zero_grad()
        outputs = autoencoder(noisy_imgs)
        loss = criterion(outputs, clean_imgs)
        loss.backward()
        optimizer.step()

        train_loss += loss.item() * noisy_imgs.size(0)
    
    train_loss /= len(train_loader.dataset)
    print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}")
Epoch 1/75, Train Loss: 0.009811
Epoch 2/75, Train Loss: 0.008472
Epoch 3/75, Train Loss: 0.008444
Epoch 4/75, Train Loss: 0.008429
Epoch 5/75, Train Loss: 0.008410
Epoch 6/75, Train Loss: 0.008404
Epoch 7/75, Train Loss: 0.008477
Epoch 8/75, Train Loss: 0.008407
Epoch 9/75, Train Loss: 0.008392
Epoch 10/75, Train Loss: 0.008385
Epoch 11/75, Train Loss: 0.008387
Epoch 12/75, Train Loss: 0.008371
Epoch 13/75, Train Loss: 0.008376
Epoch 14/75, Train Loss: 0.008360
Epoch 15/75, Train Loss: 0.008405
Epoch 16/75, Train Loss: 0.008365
Epoch 17/75, Train Loss: 0.008383
Epoch 18/75, Train Loss: 0.008347
Epoch 19/75, Train Loss: 0.008382
Epoch 20/75, Train Loss: 0.008362
Epoch 21/75, Train Loss: 0.008310
Epoch 22/75, Train Loss: 0.008315
Epoch 23/75, Train Loss: 0.008346
Epoch 24/75, Train Loss: 0.008340
Epoch 25/75, Train Loss: 0.008341
Epoch 26/75, Train Loss: 0.008316
Epoch 27/75, Train Loss: 0.008352
Epoch 28/75, Train Loss: 0.008284
Epoch 29/75, Train Loss: 0.008303
Epoch 30/75, Train Loss: 0.008362
Epoch 31/75, Train Loss: 0.008355
Epoch 32/75, Train Loss: 0.008294
Epoch 33/75, Train Loss: 0.008295
Epoch 34/75, Train Loss: 0.008281
Epoch 35/75, Train Loss: 0.008271
Epoch 36/75, Train Loss: 0.008303
Epoch 37/75, Train Loss: 0.008258
Epoch 38/75, Train Loss: 0.008333
Epoch 39/75, Train Loss: 0.008255
Epoch 40/75, Train Loss: 0.008251
Epoch 41/75, Train Loss: 0.008262
Epoch 42/75, Train Loss: 0.008236
Epoch 43/75, Train Loss: 0.008317
Epoch 44/75, Train Loss: 0.008231
Epoch 45/75, Train Loss: 0.008293
Epoch 46/75, Train Loss: 0.008237
Epoch 47/75, Train Loss: 0.008219
Epoch 48/75, Train Loss: 0.008232
Epoch 49/75, Train Loss: 0.008231
Epoch 50/75, Train Loss: 0.008222
Epoch 51/75, Train Loss: 0.008225
Epoch 52/75, Train Loss: 0.008232
Epoch 53/75, Train Loss: 0.008245
Epoch 54/75, Train Loss: 0.008232
Epoch 55/75, Train Loss: 0.008212
Epoch 56/75, Train Loss: 0.008198
Epoch 57/75, Train Loss: 0.008302
Epoch 58/75, Train Loss: 0.008193
Epoch 59/75, Train Loss: 0.008214
Epoch 60/75, Train Loss: 0.008228
Epoch 61/75, Train Loss: 0.008219
Epoch 62/75, Train Loss: 0.008198
Epoch 63/75, Train Loss: 0.008171
Epoch 64/75, Train Loss: 0.008206
Epoch 65/75, Train Loss: 0.008176
Epoch 66/75, Train Loss: 0.008193
Epoch 67/75, Train Loss: 0.008170
Epoch 68/75, Train Loss: 0.008198
Epoch 69/75, Train Loss: 0.008173
Epoch 70/75, Train Loss: 0.008194
Epoch 71/75, Train Loss: 0.008145
Epoch 72/75, Train Loss: 0.008219
Epoch 73/75, Train Loss: 0.008164
Epoch 74/75, Train Loss: 0.008155
Epoch 75/75, Train Loss: 0.008187
In [22]:
num_images = 10
indices = np.arange(num_images)  # prend simplement les 10 premiers indices

# Sélection des images bruitées et propres
noisy_imgs = x_test_noisy[indices].to('cuda')
clean_imgs = x_test[indices].to('cuda')

autoencoder.eval()
with torch.no_grad():
    reconstructed_imgs = autoencoder(noisy_imgs)

# Passage en numpy et réarrangement des dimensions pour matplotlib (CHW -> HWC)
noisy_imgs = noisy_imgs.cpu().permute(0, 2, 3, 1).numpy()
clean_imgs = clean_imgs.cpu().permute(0, 2, 3, 1).numpy()
reconstructed_imgs = reconstructed_imgs.cpu().permute(0, 2, 3, 1).numpy()

plt.figure(figsize=(20, 6))
for i in range(num_images):
    ax = plt.subplot(3, num_images, i + 1)
    plt.imshow(noisy_imgs[i])
    plt.title("Bruitée")
    plt.axis("off")

    ax = plt.subplot(3, num_images, i + 1 + num_images)
    plt.imshow(clean_imgs[i])
    plt.title("Originale")
    plt.axis("off")

    ax = plt.subplot(3, num_images, i + 1 + 2 * num_images)
    plt.imshow(reconstructed_imgs[i])
    plt.title("Reconstruite")
    plt.axis("off")

plt.tight_layout()
plt.show()
No description has been provided for this image

Denoising Autoencoder (DAE) basé sur U-Net¶

Nous allons désormais utiliser un modèle U-Net pour le Denoising Autoencoder. Le U-Net est un type d'architecture de réseau de neurones convolutifs qui est particulièrement efficace pour les tâches de segmentation d'images, mais il peut également être utilisé pour la reconstruction d'images.

In [23]:
class AutoencoderUNet(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.enc1 = self.block(3, 64)
        self.enc2 = self.block(64, 128)
        self.enc3 = self.block(128, 256)

        self.middle = self.block(256, 512)

        self.up3 = self.upsample(512, 256)  # middle (512) -> e3 (256)
        self.up2 = self.upsample(256, 128)  # d3 (256) -> e2 (128)
        self.up1 = self.upsample(128, 64)   # d2 (128) -> e1 (64)

        self.final = nn.Conv2d(64, 3, kernel_size=1)
        
    def block(self, in_ch, out_ch):
        return nn.Sequential(
            nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(),
            nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU()
        )
    
    def upsample(self, in_ch, out_ch):
        return nn.Sequential(
            nn.ConvTranspose2d(in_ch, out_ch, kernel_size=2, stride=2), 
            nn.ReLU()
        )


    def forward(self, x):
        e1 = self.enc1(x)
        e2 = self.enc2(F.max_pool2d(e1, 2))
        e3 = self.enc3(F.max_pool2d(e2, 2))

        m = self.middle(F.max_pool2d(e3, 2))

        d3 = self.up3(m)
        if d3.shape != e3.shape:
            d3 = F.interpolate(d3, size=e3.shape[2:])
        d3 = d3 + e3

        d2 = self.up2(d3)
        if d2.shape != e2.shape:
            d2 = F.interpolate(d2, size=e2.shape[2:])
        d2 = d2 + e2

        d1 = self.up1(d2)
        if d1.shape != e1.shape:
            d1 = F.interpolate(d1, size=e1.shape[2:])
        d1 = d1 + e1


        out = torch.sigmoid(self.final(d1))
        return out


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
autoencoder_unet = AutoencoderUNet().to(device)

# Optimiseur + loss
optimizer = torch.optim.Adam(autoencoder_unet.parameters()) # ou NAdam, à voir
criterion = nn.MSELoss()
In [24]:
batch_size=32

train_dataset = TensorDataset(x_train_noisy, x_train)  # input noisy, target clean

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=False,num_workers=0 )
In [25]:
epochs = 25

autoencoder_unet.train()
for epoch in range(epochs):
    train_loss = 0
    for noisy_imgs, clean_imgs in train_loader:
        noisy_imgs = noisy_imgs.to('cuda')
        clean_imgs = clean_imgs.to('cuda')
        
        optimizer.zero_grad()
        outputs = autoencoder_unet(noisy_imgs)
        loss = criterion(outputs, clean_imgs)
        loss.backward()
        optimizer.step()

        train_loss += loss.item() * noisy_imgs.size(0)
    
    train_loss /= len(train_loader.dataset)
    print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}")
Epoch 1/25, Train Loss: 0.004538
Epoch 2/25, Train Loss: 0.003361
Epoch 3/25, Train Loss: 0.003143
Epoch 4/25, Train Loss: 0.003004
Epoch 5/25, Train Loss: 0.002906
Epoch 6/25, Train Loss: 0.002830
Epoch 7/25, Train Loss: 0.002720
Epoch 8/25, Train Loss: 0.002630
Epoch 9/25, Train Loss: 0.002580
Epoch 10/25, Train Loss: 0.002514
Epoch 11/25, Train Loss: 0.002442
Epoch 12/25, Train Loss: 0.002395
Epoch 13/25, Train Loss: 0.002354
Epoch 14/25, Train Loss: 0.002318
Epoch 15/25, Train Loss: 0.002294
Epoch 16/25, Train Loss: 0.002270
Epoch 17/25, Train Loss: 0.002248
Epoch 18/25, Train Loss: 0.002223
Epoch 19/25, Train Loss: 0.002203
Epoch 20/25, Train Loss: 0.002181
Epoch 21/25, Train Loss: 0.002169
Epoch 22/25, Train Loss: 0.002157
Epoch 23/25, Train Loss: 0.002145
Epoch 24/25, Train Loss: 0.002134
Epoch 25/25, Train Loss: 0.002125
In [26]:
# Passer le modèle en mode évaluation
autoencoder_unet.eval()

# On prend les 10 premières images du test
original_images = x_test[:10].to('cuda')

# Prédictions
with torch.no_grad():
    reconstructed_imgs_unet = autoencoder_unet(original_images)

# On remet sur CPU et on convertit en numpy (channels_last)
original_images = original_images.cpu().permute(0, 2, 3, 1).numpy()
reconstructed_imgs_unet = reconstructed_imgs_unet.cpu().permute(0, 2, 3, 1).numpy()

plt.figure(figsize=(20, 4))
for i in range(10):
    # Image originale
    ax = plt.subplot(2, 10, i + 1)
    plt.imshow(original_images[i])
    plt.axis('off')

    # Image reconstruite
    ax = plt.subplot(2, 10, i + 1 + 10)
    plt.imshow(reconstructed_imgs_unet[i])
    plt.axis('off')

plt.show()
No description has been provided for this image

Nous voyons que ce modèle est capable de reconstruire les images de manière plus nette et précise que l'autoencodeur précédent. Les détails sont mieux préservés, ce qui montre que le U-Net est plus adapté pour cette tâche de reconstruction d'images. Cependant, il manque toujours des détails (pas mal de flou).

Open-Source Model : SDXL¶

SDXL est un modèle de génération d’images à partir de texte (text-to-image) basé sur les diffusion models.

Le modèle reçoit une entrée avec bruit et va distinguer les images au sein de ce bruit, selon le coefficient de guidage (guidance) à partir d'un prompt, et d'un paramètre strength indiquant à quel point on veut rester proche de l'entrée, améliorant l'image étape par étape.

In [27]:
import torch

print("CUDA dispo ?", torch.cuda.is_available())
print("Nom du GPU :", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "Pas de GPU")
CUDA dispo ? True
Nom du GPU : NVIDIA GeForce RTX 4080 SUPER
In [28]:
import torch
from diffusers import StableDiffusionXLImg2ImgPipeline
from diffusers.utils import load_image

pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-refiner-1.0", torch_dtype=torch.float16, variant="fp16", use_safetensors=True
)
pipe = pipe.to("cuda")
#url = "https://huggingface.co/datasets/patrickvonplaten/images/resolve/main/aa_xl/000000009.png"

init_image = x_train_noisy[0]  # Use the first noisy image as the initial image
C:\Users\maxka\anaconda3\envs\dl_clean\lib\site-packages\h5py\__init__.py:36: UserWarning: h5py is running against HDF5 1.14.6 when it was built against 1.14.5, this may cause problems
  _warn(("h5py is running against HDF5 {0} when it was built against {1}, "
Loading pipeline components...: 100%|████████████████████████████████████████████████████| 5/5 [00:00<00:00, 10.06it/s]

Prompt¶

Cela permet à SDXL de reconstruire l'image guidée sémantiquement, par exemple en restaurant les traits d’un chat même si l’image de base est bruitée.

In [29]:
prompt = "a clean photo of a cat"
strength = 0.02  # Contrôle combien on se rapproche du prompt (1 = complètement nouveau, 0 = identique)

result = pipe(prompt=prompt, image=init_image, strength=strength, guidance_scale=7.5) # guidance_scale, à quel point on suit le prompt, plus c'est élevé, plus c'est strict

output_image = result.images[0]
100%|████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  5.13it/s]
In [30]:
plt.figure(figsize=(15,5))

plt.subplot(1,3,1)
plt.title("Image bruitée")
plt.imshow(np.transpose(x_train_noisy[0], (1, 2, 0)))

plt.subplot(1,3,2)
plt.title("Image originale")
plt.imshow(np.transpose(x_train[0], (1, 2, 0)))

plt.subplot(1,3,3)
plt.title("SDXL")
plt.imshow(output_image)

plt.show()
No description has been provided for this image

Nous pouvons voir ici que le modèle n'est pas très performant pour les images bruitées. Le coeur du problème est que le modèle SDXL a été entraîné sur des images de haute qualité et n'est pas conçu pour gérer des images bruitées. Il est donc important de prétraiter les images (par exemple avec U-Net) avant de les passer au modèle et de modifier les paramètres de génération pour obtenir de meilleurs résultats.

Convertir un tensor PyTorch (C, H, W) → PIL Image (RGB)¶

In [31]:
def tensor_to_pil(tensor):
    """
    PyTorch tensor CHW (valeurs entre 0-1) -> PIL Image
    """
    tensor = tensor.detach().cpu()
    array = tensor.permute(1, 2, 0).numpy()
    array = (array * 255).astype(np.uint8)
    return Image.fromarray(array)

def numpy_to_pil(img_array):
    """
    NumPy image HWC float32 (valeurs entre 0-1) -> PIL Image
    """
    img = np.clip(img_array, 0, 1)
    img = (img * 255).astype(np.uint8)
    return Image.fromarray(img)

SDXL sur images réelles¶

Ici on reprend les images réelles. On aura donc une modification faite à partir du prompt : principalement sur la fourure, beaucoup plus soft et détaillée. Similairement, des yeux vont être ajoutés aux chats les ayant fermés.

In [32]:
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters))  # Choisir les mêmes pour cohérence

for idx in indices:
    init_image = tensor_to_pil(x_test[idx])
    init_image = init_image.resize((512, 512))  # Resize pour SDXL

    prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
    
    result = pipe(prompt=prompt, image=init_image, strength=0.15, guidance_scale=10)
    sdxl_image_r = result.images[0]
    sdxl_images.append(sdxl_image_r)
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 15.49it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.23it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.32it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.37it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.52it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 17.99it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.37it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.42it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.37it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.09it/s]
In [33]:
n_values_parameters = 10
indices = list(range(n_values_parameters)) 

plt.figure(figsize=(20, n_values_parameters * 2))

for i, idx in enumerate(indices):
    # Convertir les images
    original = tensor_to_pil(x_test[idx])  # PyTorch tensor → PIL
    noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
    autoenc = numpy_to_pil(reconstructed_imgs[idx]) # NumPy → PIL
    sdxl = sdxl_images[idx].resize((150, 150))  # déjà PIL, resize si besoin

    plt.subplot(n_values_parameters, 4, i * 4 + 1)
    plt.imshow(original)
    plt.title("Originale")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 4)
    plt.imshow(sdxl)
    plt.title("SDXL")
    plt.axis("off")

plt.tight_layout()
plt.show()
No description has been provided for this image

SDXL sur images bruitées¶

Ici on reprend les images bruitées et on va essayer de revenir davantage vers une image normale en utilisant SDXL le plus que possible. Il faut donc avoir un paramètre strength bas pour rester aussi fidèle que possible.

In [34]:
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters))  # Choisir les mêmes pour cohérence

for idx in indices:
    init_image = tensor_to_pil(x_test_noisy[idx])
    init_image = init_image.resize((512, 512))  # Resize pour SDXL

    prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
    
    result = pipe(prompt=prompt, image=init_image, strength=0.05, guidance_scale=9)
    sdxl_image = result.images[0]
    sdxl_images.append(sdxl_image)
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.99it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 20.62it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 21.05it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 21.28it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.26it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 18.18it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.73it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.81it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s]
In [35]:
n_values_parameters = 10
indices = list(range(n_values_parameters)) 

plt.figure(figsize=(20, n_values_parameters * 2))

for i, idx in enumerate(indices):
    # Convertir les images
    original = tensor_to_pil(x_test[idx])  # PyTorch tensor → PIL
    noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
    autoenc = numpy_to_pil(reconstructed_imgs[idx]) # NumPy → PIL
    sdxl = sdxl_images[idx].resize((150, 150))  # déjà PIL, resize si besoin

    plt.subplot(n_values_parameters, 4, i * 4 + 1)
    plt.imshow(original)
    plt.title("Originale")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 2)
    plt.imshow(noisy)
    plt.title("Bruitée")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 4)
    plt.imshow(sdxl)
    plt.title("SDXL")
    plt.axis("off")

plt.tight_layout()
plt.show()
No description has been provided for this image

Les résultats sont très bons mais le denoising n'est pas parfait.

SDXL sur images après DAE - Classique¶

In [36]:
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters))  # Choisir les mêmes pour cohérence

for idx in indices:
    dae_image = numpy_to_pil(reconstructed_imgs[idx])  # reconstruite
    dae_image = dae_image.resize((512, 512))  # Resize pour SDXL

    prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"

    result = pipe(prompt=prompt, image=dae_image, strength=0.15, guidance_scale=10)
    sdxl_from_dae = result.images[0]
    sdxl_images.append(sdxl_from_dae)
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.57it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.57it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.52it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 17.90it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.47it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.62it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 17.90it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.18it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.32it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.04it/s]
In [37]:
n_values_parameters = 10
indices = list(range(n_values_parameters)) 

plt.figure(figsize=(20, n_values_parameters * 2))

for i, idx in enumerate(indices):
    # Convertir les images
    original = tensor_to_pil(x_test[idx])  # PyTorch tensor → PIL
    noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
    autoenc = numpy_to_pil(reconstructed_imgs[idx]) # NumPy → PIL
    sdxl = sdxl_images[idx].resize((150, 150))  # déjà PIL, resize si besoin

    plt.subplot(n_values_parameters, 4, i * 4 + 1)
    plt.imshow(original)
    plt.title("Originale")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 2)
    plt.imshow(noisy)
    plt.title("Bruitée")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 3)
    plt.imshow(autoenc)
    plt.title("DAE")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 4)
    plt.imshow(sdxl)
    plt.title("SDXL")
    plt.axis("off")

plt.tight_layout()
plt.show()
No description has been provided for this image

Les images sont très mauvaises car la strength du modèle est basse. Le modèle reste donc assez fidèle à l'image d'entrée. Cependant, cette entrée est elle-même très mauvaise.

SDXL sur images après DAE - UNet¶

Ici on va utiliser SDXL pour ajouter des détails pouvant être manquants en sortie du DAE utilisant UNet.

In [38]:
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters))  # Choisir les mêmes pour cohérence

for idx in indices:
    dae_unet_image = numpy_to_pil(reconstructed_imgs_unet[idx])  # reconstruite
    dae_unet_image = dae_unet_image.resize((512, 512))  # Resize pour SDXL

    prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"

    result = pipe(prompt=prompt, image=dae_unet_image, strength=0.10, guidance_scale=8)
    sdxl_from_dae_unet = result.images[0]
    sdxl_images.append(sdxl_from_dae_unet)
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.87it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.73it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.23it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.23it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.59it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.45it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.94it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 17.86it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.30it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.59it/s]
In [39]:
n_values_parameters = 10
indices = list(range(n_values_parameters)) 

plt.figure(figsize=(20, n_values_parameters * 2))

for i, idx in enumerate(indices):
    # Convertir les images
    original = tensor_to_pil(x_test[idx])  # PyTorch tensor → PIL
    noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
    autoenc = numpy_to_pil(reconstructed_imgs_unet[idx]) # NumPy → PIL
    sdxl = sdxl_images[idx].resize((150, 150))  # déjà PIL, resize si besoin

    plt.subplot(n_values_parameters, 4, i * 4 + 1)
    plt.imshow(original)
    plt.title("Originale")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 2)
    plt.imshow(noisy)
    plt.title("Bruitée")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 3)
    plt.imshow(autoenc)
    plt.title("DAE_UNet")
    plt.axis("off")

    plt.subplot(n_values_parameters, 4, i * 4 + 4)
    plt.imshow(sdxl)
    plt.title("SDXL")
    plt.axis("off")

plt.tight_layout()
plt.show()
No description has been provided for this image

Les images sont ici assez nettes même si un flou reste présent. Le modèle SDXL a réussi à ajouter des détails manquants sur certaines images.

Essayons différentes valeurs de strength et de guidance pour voir l'impact sur la sortie.

In [40]:
sdxl_images = {}
n_values_parameters = 8
indices = list(range(n_values_parameters))  # Choisir les mêmes pour cohérence

strengths = np.linspace(0.05, 0.5, n_values_parameters)  # Différents niveaux de force
guidance_scales = np.linspace(1, 10, n_values_parameters)  # Différents niveaux de guidance

for s in strengths:
    for g in guidance_scales:
        dae_unet_image = numpy_to_pil(reconstructed_imgs_unet[idx])  # reconstruite
        dae_unet_image = dae_unet_image.resize((512, 512))  # Resize pour SDXL

        prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"

        result = pipe(prompt=prompt, image=dae_unet_image, strength=s, guidance_scale=g)
        sdxl_from_dae_unet = result.images[0]
        sdxl_images[(s, g)] = sdxl_from_dae_unet
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 16.13it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 20.62it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 21.98it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.47it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.99it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.26it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 24.15it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.94it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 17.99it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.01it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.12it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.59it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.73it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.73it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 24.69it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.74it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.98it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 18.10it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 18.26it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.70it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.98it/s]
100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.94it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 25.21it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.65it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.60it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.54it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.47it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.47it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.52it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.67it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 24.04it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.40it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.40it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.32it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.42it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.48it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.44it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.30it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:00<00:00, 25.10it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.44it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.41it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.32it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.36it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.39it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.01it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 16.92it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:00<00:00, 24.45it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.33it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.16it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.54it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.46it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.56it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.56it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.07it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:00<00:00, 25.08it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.31it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 16.88it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.14it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 16.86it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.11it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.18it/s]
100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 16.90it/s]
In [41]:
fig, axs = plt.subplots(n_values_parameters, n_values_parameters, figsize=(25, 20))

for i, s in enumerate(strengths):
    for j, g in enumerate(guidance_scales):
        ax = axs[i, j]
        sdxl_image = sdxl_images[(s, g)].resize((150, 150))  # Resize pour affichage
        ax.imshow(sdxl_image)
        ax.set_title(f"Strength: {s:.2f}, Guidance: {g:.2f}")
        ax.axis("off")
plt.tight_layout()
plt.show()
No description has been provided for this image

On peut remarquer qu'une strength élevée va faire que le modèle crée des images plus proches du prompt uniquement, en essayant de créer une image de toute pièce, tandis qu'une strength faible va essayer de rester fidèle à l'image d'entrée.

Finalement...¶

On obtient des résultats intéressants via le UNet et SDXL. On pourrait encore altérer les paramètres et les configurations pour tendre vers des résultats plus intéressants. De même on pourrait essayer d'augmenter la taille de l'espace latent de l'autoencoder ou encore modifier les hyperparamètres des couches de convolution pour obtenir des images moins bruitées en sortie.

Mais au final ça reste des chats. Et même si le résultat n'est pas des plus satisfaisants, le chemin l'a été.

DISCLAIMER !!! Regarder les images de chats qui suivent pourrait être troublant pour certains. Si vous jugez ne pas être capable de subir ce CHATiment, veuillez ne pas dépasser la ligne suivante.


Voici quelques chats générés dont on se souviendra (vous de même... si vous osez descendre dans le document):

Hall of Shame ฅ^•ﻌ•^ฅ¶

In [49]:
from IPython.display import Image, display, Markdown
import random

titles = [
    "Chatastrophe Numérique",
    "Miaul-heureusement raté",
    "Erreur Cat cent Cat",
    "Paw-norama de l’échec"
]

image_paths = ["./img/A.png", "./img/B.png", "./img/C.png", "./img/D.png", "./img/E.png"]

for path in image_paths:
    title = random.choice(titles)
    display(Markdown(f"### 🐾 *{title}*"))
    display(Image(path))

🐾 Erreur Cat cent Cat¶

No description has been provided for this image

🐾 Chatastrophe Numérique¶

No description has been provided for this image

🐾 Chatastrophe Numérique¶

No description has been provided for this image

🐾 Paw-norama de l’échec¶

No description has been provided for this image

🐾 Miaul-heureusement raté¶

No description has been provided for this image

HTML¶

In [51]:
!jupyter nbconvert --to html main.ipynb --output DeepLearning_KAMIONKA_BENALI.html
[NbConvertApp] Converting notebook main.ipynb to html
[NbConvertApp] WARNING | Alternative text is missing on 15 image(s).
[NbConvertApp] Writing 20390574 bytes to DeepLearning_KAMIONKA_BENALI.html
In [ ]: